{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f411b65",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp oauth"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0865b3d0",
   "metadata": {},
   "source": [
    "# OAuth\n",
    "> Basic scaffolding for handling OAuth\n",
    "\n",
    "- eval: false\n",
    "- skip_exec: true"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "507cd009",
   "metadata": {},
   "source": [
    "See the [docs page](https://www.fastht.ml/docs/explains/oauth.html) for an explanation of how to use this."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "793722f2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from fasthtml.common import *\n",
    "from oauthlib.oauth2 import WebApplicationClient\n",
    "from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote\n",
    "import secrets, httpx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b560f0c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "from nbdev.showdoc import show_doc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "526a5ac8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import Markdown"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a078133",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class _AppClient(WebApplicationClient):\n",
    "    id_key = 'sub'\n",
    "    def __init__(self, client_id, client_secret, code=None, scope=None, **kwargs):\n",
    "        super().__init__(client_id, code=code, scope=scope, **kwargs)\n",
    "        self.client_secret = client_secret"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d82ea17d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class GoogleAppClient(_AppClient):\n",
    "    \"A `WebApplicationClient` for Google oauth2\"\n",
    "    base_url = \"https://accounts.google.com/o/oauth2/v2/auth\"\n",
    "    token_url = \"https://oauth2.googleapis.com/token\"\n",
    "    info_url = \"https://openidconnect.googleapis.com/v1/userinfo\"\n",
    "    \n",
    "    def __init__(self, client_id, client_secret, code=None, scope=None, project_id=None, **kwargs):\n",
    "        scope_pre = \"https://www.googleapis.com/auth/userinfo\"\n",
    "        if not scope: scope=[\"openid\", f\"{scope_pre}.email\", f\"{scope_pre}.profile\"]\n",
    "        super().__init__(client_id, client_secret, code=code, scope=scope, **kwargs)\n",
    "        self.project_id = project_id\n",
    "    \n",
    "    @classmethod\n",
    "    def from_file(cls, fname, code=None, scope=None, **kwargs):\n",
    "        cred = Path(fname).read_json()['web']\n",
    "        return cls(cred['client_id'], client_secret=cred['client_secret'], project_id=cred['project_id'],\n",
    "                  code=code, scope=scope, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "371ab1f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class GitHubAppClient(_AppClient):\n",
    "    \"A `WebApplicationClient` for GitHub oauth2\"\n",
    "    prefix = \"https://github.com/login/oauth/\"\n",
    "    base_url = f\"{prefix}authorize\"\n",
    "    token_url = f\"{prefix}access_token\"\n",
    "    info_url = \"https://api.github.com/user\"\n",
    "    id_key = 'id'\n",
    "\n",
    "    def __init__(self, client_id, client_secret, code=None, scope=None, **kwargs):\n",
    "        super().__init__(client_id, client_secret, code=code, scope=scope, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e79f996",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class HuggingFaceClient(_AppClient):\n",
    "    \"A `WebApplicationClient` for HuggingFace oauth2\"\n",
    "    prefix = \"https://huggingface.co/oauth/\"\n",
    "    base_url = f\"{prefix}authorize\"\n",
    "    token_url = f\"{prefix}token\"\n",
    "    info_url = f\"{prefix}userinfo\"\n",
    "    \n",
    "    def __init__(self, client_id, client_secret, code=None, scope=None, state=None, **kwargs):\n",
    "        if not scope: scope=[\"openid\",\"profile\"]\n",
    "        if not state: state=secrets.token_urlsafe(16)\n",
    "        super().__init__(client_id, client_secret, code=code, scope=scope, state=state, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f037bd6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class DiscordAppClient(_AppClient):\n",
    "    \"A `WebApplicationClient` for Discord oauth2\"\n",
    "    base_url = \"https://discord.com/oauth2/authorize\"\n",
    "    token_url = \"https://discord.com/api/oauth2/token\"\n",
    "    revoke_url = \"https://discord.com/api/oauth2/token/revoke\"\n",
    "    info_url = \"https://discord.com/api/users/@me\"\n",
    "    id_key = 'id'\n",
    "\n",
    "    def __init__(self, client_id, client_secret, is_user=False, perms=0, scope=None, **kwargs):\n",
    "        if not scope: scope=\"applications.commands applications.commands.permissions.update identify\"\n",
    "        self.integration_type = 1 if is_user else 0\n",
    "        self.perms = perms\n",
    "        super().__init__(client_id, client_secret, scope=scope, **kwargs)\n",
    "\n",
    "    def login_link(self, redirect_uri=None, scope=None, state=None):\n",
    "        use_scope = scope or self.scope\n",
    "        d = dict(response_type='code', client_id=self.client_id,\n",
    "                 integration_type=self.integration_type, scope=use_scope)\n",
    "        if state: d['state'] = state\n",
    "        if redirect_uri: d['redirect_uri'] = redirect_uri\n",
    "        return f'{self.base_url}?' + urlencode(d)\n",
    "\n",
    "    def parse_response(self, code, redirect_uri=None):\n",
    "        headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n",
    "        data = dict(grant_type='authorization_code', code=code)\n",
    "        if redirect_uri: data['redirect_uri'] = redirect_uri\n",
    "        r = httpx.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret))\n",
    "        r.raise_for_status()\n",
    "        self.parse_request_body_response(r.text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "276520b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Auth0AppClient(_AppClient):\n",
    "    \"A `WebApplicationClient` for Auth0 OAuth2\"\n",
    "    def __init__(self, domain, client_id, client_secret, code=None, scope=None, redirect_uri=\"\", **kwargs):\n",
    "        self.redirect_uri,self.domain = redirect_uri,domain\n",
    "        config = self._fetch_openid_config()\n",
    "        self.base_url,self.token_url,self.info_url = config[\"authorization_endpoint\"],config[\"token_endpoint\"],config[\"userinfo_endpoint\"]\n",
    "        super().__init__(client_id, client_secret, code=code, scope=scope, redirect_uri=redirect_uri, **kwargs)\n",
    "\n",
    "    def _fetch_openid_config(self):\n",
    "        r = httpx.get(f\"https://{self.domain}/.well-known/openid-configuration\")\n",
    "        r.raise_for_status()\n",
    "        return r.json()\n",
    "\n",
    "    def login_link(self, req):\n",
    "        d = dict(response_type=\"code\", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri))\n",
    "        return f\"{self.base_url}?{urlencode(d)}\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "109bc501",
   "metadata": {},
   "outputs": [],
   "source": [
    "# cli = GoogleAppClient.from_file('client_secret.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "990d2310",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None, **kwargs):\n",
    "    \"Get a login link for this client\"\n",
    "    if not scope: scope=self.scope\n",
    "    if not state: state=getattr(self, 'state', None)\n",
    "    return self.prepare_request_uri(self.base_url, redirect_uri, scope, state=state, **kwargs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42ee9991",
   "metadata": {},
   "source": [
    "Generating a login link that sends the user to the OAuth provider is done with `client.login_link()`.\n",
    "\n",
    "It can sometimes be useful to pass state to the OAuth provider, so that when the user returns you can pick up where they left off. This can be done by passing the `state` parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f4dff5df",
   "metadata": {},
   "outputs": [],
   "source": [
    "from fasthtml.jupyter import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bc9ce02a",
   "metadata": {},
   "outputs": [],
   "source": [
    "redir_path = '/redirect'\n",
    "port = 8000"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b4dd67a9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<script>\n",
       "document.body.addEventListener('htmx:configRequest', (event) => {\n",
       "    if(event.detail.path.includes('://')) return;\n",
       "    htmx.config.selfRequestsOnly=false;\n",
       "    event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;\n",
       "});\n",
       "</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "app,rt = fast_app()\n",
    "server = JupyUvi(app, port=port)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "551454f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def get_host(request):\n",
    "    \"\"\"Get the host, preferring X-Forwarded-Host if available\"\"\"\n",
    "    forwarded_host = request.headers.get('x-forwarded-host')\n",
    "    return forwarded_host if forwarded_host else request.url.netloc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3cc9b978",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Without X-Forwarded-Host: localhost:8000\n",
      "With X-Forwarded-Host: example.com\n"
     ]
    }
   ],
   "source": [
    "from types import SimpleNamespace\n",
    "from urllib.parse import urlparse\n",
    "\n",
    "mock_request_localhost = SimpleNamespace(headers={}, url=SimpleNamespace(netloc='localhost:8000'))\n",
    "mock_request_with_forward = SimpleNamespace(\n",
    "    headers={'x-forwarded-host': 'example.com'}, \n",
    "    url=SimpleNamespace(netloc='localhost:8000', hostname='localhost')\n",
    ")\n",
    "\n",
    "print(\"Without X-Forwarded-Host:\", get_host(mock_request_localhost))\n",
    "print(\"With X-Forwarded-Host:\", get_host(mock_request_with_forward))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0e2da0f",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def redir_url(req, redir_path, scheme=None):\n",
    "    \"Get the redir url for the host in `request`\"\n",
    "    host = get_host(req)\n",
    "    scheme = 'http' if host.split(':')[0] in (\"localhost\", \"127.0.0.1\") else 'https'\n",
    "    return f\"{scheme}://{host}{redir_path}\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b86cd0c",
   "metadata": {},
   "outputs": [],
   "source": [
    "@rt\n",
    "def index(request):\n",
    "    redir = redir_url(request, redir_path)\n",
    "    return A('login', href=cli.login_link(redir), target='_blank')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "479878a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def parse_response(self:_AppClient, code, redirect_uri):\n",
    "    \"Get the token from the oauth2 server response\"\n",
    "    payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,\n",
    "                   client_secret=self.client_secret, grant_type='authorization_code')\n",
    "    r = httpx.post(self.token_url, data=payload)\n",
    "    r.raise_for_status()\n",
    "    self.parse_request_body_response(r.text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6967dbb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def get_info(self:_AppClient, token=None):\n",
    "    \"Get the info for authenticated user\"\n",
    "    if not token: token = self.token[\"access_token\"]\n",
    "    headers = {'Authorization': f'Bearer {token}'}\n",
    "    return httpx.get(self.info_url, headers=headers).json()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03702349",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def retr_info(self:_AppClient, code, redirect_uri):\n",
    "    \"Combines `parse_response` and `get_info`\"\n",
    "    self.parse_response(code, redirect_uri)\n",
    "    return self.get_info()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c705ea8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "@rt(redir_path)\n",
    "def get(request, code:str):\n",
    "    redir = redir_url(request, redir_path)\n",
    "    info = cli.retr_info(code, redir)\n",
    "    return P(f'Login successful for {info[\"name\"]}!')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce6f838d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# HTMX()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8c2d2261",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29f52061",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def retr_id(self:_AppClient, code, redirect_uri):\n",
    "    \"Call `retr_info` and then return id/subscriber value\"\n",
    "    return self.retr_info(code, redirect_uri)[self.id_key]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d978e813",
   "metadata": {},
   "source": [
    "After logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a `code` parameter, which is used to get an access token and fetch the user's profile information. See [the explanation here](https://www.fastht.ml/docs/explains/oauth.html) for a worked example. You can either:\n",
    "\n",
    "- Use client.retr_info(code) to get all the profile information, or\n",
    "- Use client.retr_id(code) to get just the user's ID.\n",
    "\n",
    "After either of these calls, you can also access the access token (used to revoke access, for example) with `client.token[\"access_token\"]`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b96e009",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "http_patterns = (r'^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',)\n",
    "def url_match(request, patterns=http_patterns):\n",
    "    return any(re.match(pattern, get_host(request).split(':')[0]) for pattern in patterns)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fee2db6c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Localhost: http://localhost:8000/redirect\n",
      "With X-Forwarded-Host: https://example.com/redirect\n",
      "Production: https://myapp.com/redirect\n"
     ]
    }
   ],
   "source": [
    "from types import SimpleNamespace\n",
    "from urllib.parse import urlparse\n",
    "\n",
    "mock_request_prod = SimpleNamespace(headers={}, url=SimpleNamespace(netloc='myapp.com', hostname='myapp.com'))\n",
    "\n",
    "print(\"Localhost:\", redir_url(mock_request_localhost, '/redirect'))\n",
    "print(\"With X-Forwarded-Host:\", redir_url(mock_request_with_forward, '/redirect'))\n",
    "print(\"Production:\", redir_url(mock_request_prod, '/redirect'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dda68390",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class OAuth:\n",
    "    def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns):\n",
    "        if not skip: skip = [redir_path,error_path,login_path]\n",
    "        store_attr()\n",
    "        def before(req, session):\n",
    "            if 'auth' not in req.scope: req.scope['auth'] = session.get('auth')\n",
    "            auth = req.scope['auth']\n",
    "            if not auth: return self.redir_login(session)\n",
    "            res = self.check_invalid(req, session, auth)\n",
    "            if res: return res\n",
    "        app.before.append(Beforeware(before, skip=skip))\n",
    "\n",
    "        @app.get(redir_path)\n",
    "        def redirect(req, session, code:str=None, error:str=None, state:str=None):\n",
    "            if not code:\n",
    "                session['oauth_error']=error\n",
    "                return RedirectResponse(self.error_path, status_code=303)\n",
    "            scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'\n",
    "            base_url = f\"{scheme}://{get_host(req)}\"\n",
    "            info = AttrDictDefault(cli.retr_info(code, base_url+redir_path))\n",
    "            ident = info.get(self.cli.id_key)\n",
    "            if not ident: return self.redir_login(session)\n",
    "            res = self.get_auth(info, ident, session, state)\n",
    "            if not res:   return self.redir_login(session)\n",
    "            req.scope['auth'] = session['auth'] = ident\n",
    "            return res\n",
    "\n",
    "        @app.get(logout_path)\n",
    "        def logout(session):\n",
    "            session.pop('auth', None)\n",
    "            return self.logout(session)\n",
    "\n",
    "    def redir_login(self, session): return RedirectResponse(self.login_path, status_code=303)\n",
    "    def redir_url(self, req):\n",
    "        scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'\n",
    "        return redir_url(req, self.redir_path, scheme)\n",
    "\n",
    "    def login_link(self, req, scope=None, state=None): return self.cli.login_link(self.redir_url(req), scope=scope, state=state)\n",
    "    def check_invalid(self, req, session, auth): return False\n",
    "    def logout(self, session): return self.redir_login(session)\n",
    "    def get_auth(self, info, ident, session, state): raise NotImplementedError()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "538de5d3",
   "metadata": {},
   "source": [
    "### Google helpers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be2990cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "try:\n",
    "    from google.oauth2.credentials import Credentials\n",
    "    from google.auth.transport.requests import Request\n",
    "except ImportError:\n",
    "    Request=None\n",
    "    class Credentials: pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4adfb6dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch()\n",
    "def consent_url(self:GoogleAppClient, proj=None):\n",
    "    \"Get Google OAuth consent screen URL\"\n",
    "    loc = \"https://console.cloud.google.com/auth/clients\"\n",
    "    if proj is None: proj=self.project_id\n",
    "    return f\"{loc}/{self.client_id}?project={proj}\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e657d9b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L223){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### GoogleAppClient.consent_url\n",
       "\n",
       ">      GoogleAppClient.consent_url (proj=None)\n",
       "\n",
       "*Get Google OAuth consent screen URL*"
      ],
      "text/plain": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L223){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### GoogleAppClient.consent_url\n",
       "\n",
       ">      GoogleAppClient.consent_url (proj=None)\n",
       "\n",
       "*Get Google OAuth consent screen URL*"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_doc(GoogleAppClient.consent_url)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7393bc12",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def update(self:Credentials):\n",
    "    \"Refresh the credentials if they are expired, and return them\"\n",
    "    if self.expired: self.refresh(Request())\n",
    "    return self"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b68863e4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L231){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### Credentials.update\n",
       "\n",
       ">      Credentials.update ()\n",
       "\n",
       "*Refresh the credentials if they are expired, and return them*"
      ],
      "text/plain": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L231){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### Credentials.update\n",
       "\n",
       ">      Credentials.update ()\n",
       "\n",
       "*Refresh the credentials if they are expired, and return them*"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_doc(Credentials.update)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ace16168",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def save(self:Credentials, fname):\n",
    "    \"Save credentials to `fname`\"\n",
    "    save_pickle(fname, self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "877a7b20",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L238){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### Credentials.save\n",
       "\n",
       ">      Credentials.save (fname)\n",
       "\n",
       "*Save credentials to `fname`*"
      ],
      "text/plain": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L238){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### Credentials.save\n",
       "\n",
       ">      Credentials.save (fname)\n",
       "\n",
       "*Save credentials to `fname`*"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_doc(Credentials.save)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ef02a88",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def load_creds(fname):\n",
    "    \"Load credentials from `fname`\"\n",
    "    return load_pickle(fname).update()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0024ab0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def creds(self:GoogleAppClient):\n",
    "    \"Create `Credentials` from the client, refreshing if needed\"\n",
    "    return Credentials(token=self.access_token, refresh_token=self.refresh_token, \n",
    "        token_uri=self.token_url, client_id=self.client_id,\n",
    "        client_secret=self.client_secret, scopes=self.scope).update()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8de24c61",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L249){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### GoogleAppClient.creds\n",
       "\n",
       ">      GoogleAppClient.creds ()\n",
       "\n",
       "*Create `Credentials` from the client, refreshing if needed*"
      ],
      "text/plain": [
       "---\n",
       "\n",
       "[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L249){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
       "\n",
       "### GoogleAppClient.creds\n",
       "\n",
       ">      GoogleAppClient.creds ()\n",
       "\n",
       "*Create `Credentials` from the client, refreshing if needed*"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_doc(GoogleAppClient.creds)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "474e14b4",
   "metadata": {},
   "source": [
    "# Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d211e8e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0f7a90b",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
